import os
import re
import math
import logging

from agent.common.system import sysCommand
from agent.common.config import *

logger = logging.getLogger('common')

class Singleton(object):
    def __init__(self, cls):
        self._cls = cls
        self._instance = {}
    
    def __call__(self):
        if self._cls not in self._instance:
            self._instance[self._cls] = self._cls()
        return self._instance[self._cls]


@Singleton
class NetInfo():
    def __init__(self):
        self.load_pci_dev()

        for virtio_dev, dev_name in self.pci_dev_list:
            try:
                self.load_bus_info(dev_name)
                self.load_queue_list(dev_name)
                self.load_interrupts(virtio_dev, dev_name)

            except Exception:
                continue
            
            else:
                self.dev_name = dev_name
                logger.info("****** confirm network dev: {} ******".format(self.dev_name))
                break
        
        assert self.dev_name

        self.cpu_num   = int(sysCommand("cat /proc/cpuinfo| grep 'physical id'| sort| uniq| wc -l"))
        self.processor = int(sysCommand("cat /proc/cpuinfo| grep 'processor'| wc -l"))
        logger.info("cpu num: {}, processor num: {}".format(self.cpu_num, self.processor))

        self.load_numa_node_core_range()
        
        if self.processor != self.numa_node_core_range[1] - self.numa_node_core_range[0] + 1:
            logger.warning("cpu number({}) unmatch with numa node number({})!".format(
                self.processor,
                self.numa_node_core_range[1] - self.numa_node_core_range[0] + 1
            ))
    
    # def get_rfs_binding_schema(self, binding_type):
    #     binding_schema = {}
    #     for _, queue_id in enumerate(self.rx_queue):
    #         binding_schema[queue_id] = (self.padding_binding_code("f" * math.ceil(self.processor/4)), 4096)
            
    #     logger.info("RFS binding schema: {}".format(binding_schema))
    #     return binding_schema
        
    def get_queue_binding_schema(self, binding_type, queue_name = "xps"):
        """ get XPS/RPS binding schema

        Args:
            binding_type (str): disperse, different or off
            queue (str): xps to tx_queue or rps to rx_queue
        """
        assert binding_type in ['disperse', 'different', 'off', 'all']
        binding_queue = self.tx_queue if queue_name == "xps" else self.rx_queue
        binding_schema = {}
        
        for queue_index, queue_id in enumerate(binding_queue):
            if binding_type == "disperse":
                bitmap = "".join(['1' if core_index % len(binding_queue) == queue_index % self.processor \
                            else '0' for core_index in range(self.processor)][::-1])
                binding_schema[queue_id] = self.padding_binding_code(hex(int(bitmap,2))[2:])
            
            elif binding_type == "different":
                _core_num = min(self.processor, len(binding_queue))
                
                if _core_num == 1:
                    bitmap = "1"
                    
                elif queue_name == "xps":
                    bitmap = "".join(['1' if core_index % len(binding_queue) == queue_index % int(_core_num/2) + int(_core_num/2) \
                                else '0' for core_index in range(_core_num)][::-1])
                
                else:
                    bitmap = "".join(['1' if core_index % len(binding_queue) == queue_index % int(_core_num/2)\
                                else '0' for core_index in range(_core_num)][::-1])
                
                logger.info("bitmap: {} {}".format(queue_id, bitmap))
                binding_schema[queue_id] = self.padding_binding_code(hex(int(bitmap,2))[2:])
            
            elif binding_type == "off":
                binding_schema[queue_id] = self.padding_binding_code("0")
            
            elif binding_type == "all":
                binding_schema[queue_id] = self.padding_binding_code("f" * math.ceil(self.processor/4))
        
        logger.info("{} '{}' binding schema: {}".format(binding_type, queue_name, binding_schema))
        return binding_schema
    
    def get_affinity_binding_schema(self, binding_type):
        def parse_interrupts(interrupts_list):
            """
            Split net interrupts to input interrupts and output interrupts

            """
            input_interrupts_list = interrupts_list[0::2]
            output_interrupts_list = interrupts_list[1::2]
            return input_interrupts_list, output_interrupts_list
        """
        Get affinity binding code for smp_affinity_list

        binding_type:
        - dedicated: core 0 for each interrupts
        - disperse: sequentially allocate core for interrupts
        - different: base on disperse, distinguishing input interrupts and output interrupts
        - off: Unspecified
        """
        assert binding_type in ['dedicated', 'disperse', 'different', 'off', 'eighth']
        
        # * dedicated: core 0 for each interrupts
        if binding_type == "dedicated":
            affinity_binding_schema = dict(zip(
                self.interrupts, 
                [self.numa_node_core_range[0],] * len(self.interrupts)))

        # * disperse: sequentially allocate core for interrupts
        elif binding_type == "disperse":
            input_interrupts_list, output_interrupts_list = parse_interrupts(self.interrupts)
            _input_affinity_binding_core  = []
            _output_affinity_binding_core = []

            for i in range(input_interrupts_list.__len__()):
                _code = self.numa_node_core_range[0] + i % (
                        self.numa_node_core_range[1] - self.numa_node_core_range[0] + 1)
                _input_affinity_binding_core.append(_code)
                _output_affinity_binding_core.append(_code)
            
            affinity_binding_schema = dict(zip(
                input_interrupts_list + output_interrupts_list,
                _input_affinity_binding_core + _output_affinity_binding_core))

        # * different: base on disperse, distinguishing input interrupts and output interrupts
        elif binding_type == "different":
            input_interrupts_list, output_interrupts_list = parse_interrupts(self.interrupts)
            input_numa_node  = [self.numa_node_core_range[0], int(self.numa_node_core_range[1]/2)]
            output_numa_node = [min(self.numa_node_core_range[1], int(self.numa_node_core_range[1]/2) + 1), self.numa_node_core_range[1]]

            _input_affinity_binding_core  = []
            _output_affinity_binding_core = []

            for i in range(input_interrupts_list.__len__()):
                _code = input_numa_node[0] + i % (
                        input_numa_node[1] - input_numa_node[0] + 1)
                _input_affinity_binding_core.append(_code)
            
            for i in range(output_interrupts_list.__len__()):
                _code = output_numa_node[0] + i % (
                        output_numa_node[1] - output_numa_node[0] + 1)
                _output_affinity_binding_core.append(_code)

            logger.debug("input affinity binding core = {}, output affinity binding core = {}".format(_input_affinity_binding_core, _output_affinity_binding_core))
            affinity_binding_schema = dict(zip(
                input_interrupts_list + output_interrupts_list,
                _input_affinity_binding_core + _output_affinity_binding_core))

        # * off: Unspecified
        elif binding_type == "off":
            affinity_binding_schema = dict(zip(self.interrupts, 
                    ["{}-{}".format(
                        self.numa_node_core_range[0], 
                        self.numa_node_core_range[1]),] * self.interrupts.__len__()))

        elif binding_type == "eighth":
            if self.processor < 8:
                # 4c or 2c, no binding specific core
                affinity_binding_schema = dict(zip(self.interrupts, 
                    ["{}-{}".format(
                        self.numa_node_core_range[0], 
                        self.numa_node_core_range[1]),] * self.interrupts.__len__()))
            
            elif self.processor == 8:
                affinity_binding_schema = dict(zip(self.interrupts, 
                    ["{}".format(self.numa_node_core_range[1]),] * self.interrupts.__len__()))
                
            else:
                binding_core_num = int(self.processor / 8)  # binding_core_num >= 1
                if self.numa_node_core_range[0] == self.numa_node_core_range[1]:
                    # Incase: self.numa_node_core_range[1] - binding_core_num < self.numa_node_core_range[0]
                    affinity_binding_schema = dict(zip(self.interrupts, 
                        ["{}".format(self.numa_node_core_range[1]),] * self.interrupts.__len__()))
                else:
                    affinity_binding_schema = dict(zip(self.interrupts, 
                        ["{}-{}".format(
                            self.numa_node_core_range[1] - binding_core_num, 
                            self.numa_node_core_range[1]),] * self.interrupts.__len__()))
           
        logger.info("smp affinity '{}' binding schema : {}".format(binding_type, affinity_binding_schema))
        return affinity_binding_schema
    
    def load_pci_dev(self):
        self.pci_dev_list = []
        for dev in sysCommand("ls /sys/class/net").split("\n"):
            _path = os.path.join("/sys/class/net", dev)
            if not os.path.isdir(_path):
                continue
            
            try:
                _link_path = os.readlink(_path)
                if re.search(r"pci\d+", _link_path):
                    if re.search(r"virtio\d", _link_path):
                        self.pci_dev_list.append((re.search(r"virtio\d", _link_path).group(0), dev))
                    else:
                        self.pci_dev_list.append((dev, dev))
                        
            except OSError:
                continue
        
        logger.info("pci device:{}".format(self.pci_dev_list))

    def load_bus_info(self, dev_name):
        pci_info = sysCommand("ethtool -i {}".format(dev_name))
        self.bus_info = re.search(r"bus-info: (.*)\n", pci_info).group(1)
        assert self.bus_info != ''
        logger.info("bus info: {}".format(self.bus_info))
    
    def load_queue_list(self, dev_name):
        queue_info = sysCommand("ls /sys/class/net/{}/queues/".format(dev_name))
        tx_pattern, rx_pattern = re.compile(r"tx-\d+"), re.compile(r"rx-\d+")
        if not re.search(tx_pattern, queue_info) or not re.search(rx_pattern, queue_info):
            raise Exception("can not find tx/rx queue in /sys/class/net/{}/queues/".format(dev_name))
        
        self.tx_queue = sorted(re.findall(tx_pattern, queue_info), key = lambda x: int(re.search(r"(\d+)", x).group(1)))
        self.rx_queue = sorted(re.findall(rx_pattern, queue_info), key = lambda x: int(re.search(r"(\d+)", x).group(1)))
        logger.info("tx queue: {}".format(self.tx_queue))
        logger.info("rx queue: {}".format(self.rx_queue))

    def load_interrupts(self, virtio_dev, dev_name):
        def parse_interrupts_code(interrupts):
            result = []
            for i in interrupts:
                interrupts_id = re.search(r"^(\d+):", i).group(1)
                result.append(interrupts_id)
            return result
        
        content = [i.strip() for i in sysCommand("cat /proc/interrupts").split('\n')]
        
        interrupts_list = [
            i for i in content if re.search("{dev_name}-input|{dev_name}-output".format(
            dev_name = dev_name),i)]
        
        if interrupts_list.__len__() == 0:
            interrupts_list = [
                i for i in content if re.search("{virtio_dev}-input|{virtio_dev}-output".format(
                virtio_dev = virtio_dev),i)]
        
        if interrupts_list.__len__() == 0:
            interrupts_list = [i for i in content if re.search("{}".format(dev_name),i)]

        assert interrupts_list.__len__() != 0
        logger.debug("parse interrupts_list: \n{}".format("\n".join(interrupts_list)))
        self.interrupts = parse_interrupts_code(interrupts_list)

        assert self.interrupts.__len__() > 0
        logger.info("interrupts: {}".format(self.interrupts))

    def load_numa_node_core_range(self):
        numa_message = sysCommand("lspci -vvvs {}".format(self.bus_info))
        if re.search(r"NUMA node: (\d)\n", numa_message):
            numa_node_num = re.search(r"NUMA node: (\d)\n", numa_message).group(1)
        else:
            numa_node_num = 0
        logger.info("get numa_node_num = {}".format(numa_node_num))

        cpu_message = sysCommand("lscpu")
        if self.processor == 1:
            pattern_en = re.compile(r"NUMA 节点{} CPU：\s*(\d+)".format(numa_node_num))
            pattern = re.compile(r"NUMA node{} CPU\(s\):\s*(\d+)".format(numa_node_num))
            if re.search(pattern, cpu_message):
                numa_node_core_range = [re.search(pattern, cpu_message).group(1),] * 2
            elif re.search(pattern_en, cpu_message):
                numa_node_core_range = [re.search(pattern_en, cpu_message).group(1),] * 2
            else:
                logger.error("can not find numa_node_core_range in process = 1")
                raise Exception("can not find numa_node_core_range in process = 1")
                
        elif self.processor == 2:
            pattern_en = re.compile(r"NUMA 节点{} CPU：\s*(\d+,\d+)".format(numa_node_num))
            pattern = re.compile(r"NUMA node{} CPU\(s\):\s*(\d+,\d+)".format(numa_node_num))
            if re.search(pattern, cpu_message):
                numa_node_core_range = re.search(pattern, cpu_message).group(1).split(',')
            elif re.search(pattern_en, cpu_message):
                numa_node_core_range = re.search(pattern_en, cpu_message).group(1).split(',')
            else:
                logger.error("can not find numa_node_core_range in process = 2")
                raise Exception("can not find numa_node_core_range in process = 2")
            
        else:
            pattern_en = re.compile(r"NUMA 节点{} CPU：\s*(\d+-\d+)".format(numa_node_num))
            pattern = re.compile(r"NUMA node{} CPU\(s\):\s*(\d+-\d+)".format(numa_node_num))
            if re.search(pattern, cpu_message):
                numa_node_core_range = re.search(pattern, cpu_message).group(1).split('-')
            elif re.search(pattern_en, cpu_message):
                numa_node_core_range = re.search(pattern_en, cpu_message).group(1).split('-')
            else:
                logger.error("can not find numa_node_core_range in process = {}".format(self.processor))
                raise Exception("can not find numa_node_core_range in process = {}".format(self.processor))
        
        self.numa_node_core_range = [int(i) for i in numa_node_core_range]
        logger.info("numa node core range: {}".format(self.numa_node_core_range))

    def padding_binding_code(self, code):
        assert type(code) is str
        
        bit_num = math.ceil(self.processor/4)
        if len(code) > bit_num:
            logger.warning("The number of code bit '{}' exceeds the current number of processor({})".format(
                code, self.processor
            ))
            return code
        
        # padding '0' before code
        code = "0" * (bit_num - len(code)) + code
        
        # split code with ','
        split_code = []
        for count, i in enumerate(code[::-1]):
            split_code.append(i)
            if (count + 1) % 8 == 0 and count != len(code) - 1:
                split_code.append(",")

        return "".join(split_code[::-1])

if __name__ == "__main__":
    net = NetInfo()
    net.get_queue_binding_schema('different','xps')
    net.get_queue_binding_schema('different','rps')
    net.get_queue_binding_schema('all','rps')